/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
package jacky.song.android.util.io;

import jacky.song.android.util.Assert;
import jacky.song.android.util.closure.Closure;
import jacky.song.android.util.closure.Processor;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;

/**
 * General file manipulation utilities.
 * <p>
 * Facilities are provided in the following areas:
 * <ul>
 * <li>writing to a file
 * <li>reading from a file
 * <li>make a directory including parent directories
 * <li>copying files and directories
 * <li>deleting files and directories
 * <li>converting to and from a URL
 * <li>comparing file content
 * <li>file last changed date
 * <li>calculating a checksum
 * </ul>
 * <p>
 * Origin of code: Excalibur, Alexandria, Commons-Utils
 * 
 * @author <a href="mailto:burton@relativity.yi.org">Kevin A. Burton</A>
 * @author <a href="mailto:sanders@apache.org">Scott Sanders</a>
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph.Reck</a>
 * @author <a href="mailto:peter@apache.org">Peter Donald</a>
 * @author <a href="mailto:jefft@apache.org">Jeff Turner</a>
 * @author Matthew Hawthorne
 * @author <a href="mailto:jeremias@apache.org">Jeremias Maerki</a>
 * @author Stephen Colebourne
 * @author Ian Springer
 * @author Chris Eldredge
 * @author Jim Harrington
 * @author Niall Pemberton
 * @author Sandy McArthur
 * @version $Id: FileUtils.java 507684 2007-02-14 20:38:25Z bayard $
 */
public abstract class FileUtils {
  
  /**
   * The number of bytes in a kilobyte.
   */
  public static final long ONE_KB = 1024;
  
  /**
   * The number of bytes in a megabyte.
   */
  public static final long ONE_MB = ONE_KB * ONE_KB;
  
  /**
   * The number of bytes in a gigabyte.
   */
  public static final long ONE_GB = ONE_KB * ONE_MB;
  
  /**
   * An empty array of type <code>File</code>.
   */
  public static final File[] EMPTY_FILE_ARRAY = new File[0];
  
  // -----------------------------------------------------------------------
  /**
   * Opens a {@link FileInputStream} for the specified file, providing better error messages than simply calling <code>new FileInputStream(file)</code>.
   * <p>
   * At the end of the method either the stream will be successfully opened, or an exception will have been thrown.
   * <p>
   * An exception is thrown if the file does not exist. An exception is thrown if the file object exists but is a directory. An exception is thrown if the file exists but cannot be read.
   * 
   * @param file
   *          the file to open for input, must not be <code>null</code>
   * @return a new {@link FileInputStream} for the specified file
   * @throws FileNotFoundException
   *           if the file does not exist
   * @throws IOException
   *           if the file object is a directory
   * @throws IOException
   *           if the file cannot be read
   * @since Commons IO 1.3
   */
  public static FileInputStream openInputStream(File file) throws IOException {
    if (file.exists()) {
      if (file.isDirectory()) { throw new IOException("File '" + file + "' exists but is a directory"); }
      if (file.canRead() == false) { throw new IOException("File '" + file + "' cannot be read"); }
    }
    else {
      throw new FileNotFoundException("File '" + file + "' does not exist");
    }
    return new FileInputStream(file);
  }
  
  // -----------------------------------------------------------------------
  /**
   * Opens a {@link FileOutputStream} for the specified file, checking and creating the parent directory if it does not exist.
   * <p>
   * At the end of the method either the stream will be successfully opened, or an exception will have been thrown.
   * <p>
   * The parent directory will be created if it does not exist. The file will be created if it does not exist. An exception is thrown if the file object exists but is a directory. An exception is thrown if the file exists but cannot be written to. An exception is thrown if the parent directory cannot be created.
   * 
   * @param file
   *          the file to open for output, must not be <code>null</code>
   * @return a new {@link FileOutputStream} for the specified file
   * @throws IOException
   *           if the file object is a directory
   * @throws IOException
   *           if the file cannot be written to
   * @throws IOException
   *           if a parent directory needs creating but that fails
   * @since Commons IO 1.3
   */
  public static FileOutputStream openOutputStream(File file) throws IOException {
    if (file.exists()) {
      if (file.isDirectory()) { throw new IOException("File '" + file + "' exists but is a directory"); }
      if (file.canWrite() == false) { throw new IOException("File '" + file + "' cannot be written to"); }
    }
    else {
      File parent = file.getParentFile();
      if (parent != null && parent.exists() == false) {
        if (parent.mkdirs() == false) { throw new IOException("File '" + file + "' could not be created"); }
      }
    }
    return new FileOutputStream(file);
  }
  
  // -----------------------------------------------------------------------
  /**
   * Returns a human-readable version of the file size, where the input represents a specific number of bytes.
   * 
   * @param size
   *          the number of bytes
   * @return a human-readable display value (includes units)
   */
  public static String byteCountToDisplaySize(long size) {
    String displaySize;
    
    if (size / ONE_GB > 0) {
      displaySize = String.valueOf(size / ONE_GB) + " GB";
    }
    else if (size / ONE_MB > 0) {
      displaySize = String.valueOf(size / ONE_MB) + " MB";
    }
    else if (size / ONE_KB > 0) {
      displaySize = String.valueOf(size / ONE_KB) + " KB";
    }
    else {
      displaySize = String.valueOf(size) + " bytes";
    }
    return displaySize;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Implements the same behaviour as the "touch" utility on Unix. It creates a new file with size 0 or, if the file exists already, it is opened and closed without modifying it, but updating the file date and time.
   * <p>
   * NOTE: As from v1.3, this method throws an IOException if the last modified date of the file cannot be set. Also, as from v1.3 this method creates parent directories if they do not exist.
   * 
   * @param file
   *          the File to touch
   * @throws IOException
   *           If an I/O problem occurs
   */
  public static void touch(File file) throws IOException {
    if (!file.exists()) {
      OutputStream out = openOutputStream(file);
      IOUtils.closeQuietly(out);
    }
    boolean success = file.setLastModified(System.currentTimeMillis());
    if (!success) { throw new IOException("Unable to set the last modification time for " + file); }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Converts a Collection containing java.io.File instanced into array representation. This is to account for the difference between File.listFiles() and FileUtils.listFiles().
   * 
   * @param files
   *          a Collection containing java.io.File instances
   * @return an array of java.io.File
   */
  public static File[] convertFileCollectionToFileArray(Collection<File> files) {
    return files.toArray(new File[files.size()]);
  }
  
  // -----------------------------------------------------------------------
  /**
   * Compare the contents of two files to determine if they are equal or not.
   * <p>
   * This method checks to see if the two files are different lengths or if they point to the same file, before resorting to byte-by-byte comparison of the contents.
   * <p>
   * Code origin: Avalon
   * 
   * @param file1
   *          the first file
   * @param file2
   *          the second file
   * @return true if the content of the files are equal or they both don't exist, false otherwise
   * @throws IOException
   *           in case of an I/O error
   */
  public static boolean contentEquals(File file1, File file2) throws IOException {
    boolean file1Exists = file1.exists();
    if (file1Exists != file2.exists()) { return false; }
    
    if (!file1Exists) {
      // two not existing files are equal
      return true;
    }
    
    if (file1.isDirectory() || file2.isDirectory()) {
      // don't want to compare directory contents
      throw new IOException("Can't compare directories, only files");
    }
    
    if (file1.length() != file2.length()) {
      // lengths differ, cannot be equal
      return false;
    }
    
    if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) {
      // same file
      return true;
    }
    
    InputStream input1 = null;
    InputStream input2 = null;
    try {
      input1 = new FileInputStream(file1);
      input2 = new FileInputStream(file2);
      return IOUtils.contentEquals(input1, input2);
      
    }
    finally {
      IOUtils.closeQuietly(input1);
      IOUtils.closeQuietly(input2);
    }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Convert from a <code>URL</code> to a <code>File</code>.
   * <p>
   * From version 1.1 this method will decode the URL. Syntax such as <code>file:///my%20docs/file.txt</code> will be correctly decoded to <code>/my docs/file.txt</code>.
   * 
   * @param url
   *          the file URL to convert, <code>null</code> returns <code>null</code>
   * @return the equivalent <code>File</code> object, or <code>null</code> if the URL's protocol is not <code>file</code>
   * @throws IllegalArgumentException
   *           if the file is incorrectly encoded
   */
  public static File toFile(URL url) {
    if (url == null || !url.getProtocol().equals("file")) { return null; }
    String filename = url.getFile().replace('/', File.separatorChar);
    int pos = 0;
    while ((pos = filename.indexOf('%', pos)) >= 0) {
      if (pos + 2 < filename.length()) {
        String hexStr = filename.substring(pos + 1, pos + 3);
        char ch = (char) Integer.parseInt(hexStr, 16);
        filename = filename.substring(0, pos) + ch + filename.substring(pos + 3);
      }
    }
    return new File(filename);
  }
  
  /**
   * Converts each of an array of <code>URL</code> to a <code>File</code>.
   * <p>
   * Returns an array of the same size as the input. If the input is <code>null</code>, an empty array is returned. If the input contains <code>null</code>, the output array contains <code>null</code> at the same index.
   * <p>
   * This method will decode the URL. Syntax such as <code>file:///my%20docs/file.txt</code> will be correctly decoded to <code>/my docs/file.txt</code>.
   * 
   * @param urls
   *          the file URLs to convert, <code>null</code> returns empty array
   * @return a non-<code>null</code> array of Files matching the input, with a <code>null</code> item if there was a <code>null</code> at that index in the input array
   * @throws IllegalArgumentException
   *           if any file is not a URL file
   * @throws IllegalArgumentException
   *           if any file is incorrectly encoded
   * @since Commons IO 1.1
   */
  public static File[] toFiles(URL[] urls) {
    if (urls == null || urls.length == 0) { return EMPTY_FILE_ARRAY; }
    File[] files = new File[urls.length];
    for (int i = 0; i < urls.length; i++) {
      URL url = urls[i];
      if (url != null) {
        if (url.getProtocol().equals("file") == false) { throw new IllegalArgumentException(
            "URL could not be converted to a File: " + url); }
        files[i] = toFile(url);
      }
    }
    return files;
  }
  
  /**
   * Converts each of an array of <code>File</code> to a <code>URL</code>.
   * <p>
   * Returns an array of the same size as the input.
   * 
   * @param files
   *          the files to convert
   * @return an array of URLs matching the input
   * @throws IOException
   *           if a file cannot be converted
   */
  public static URL[] toURLs(File[] files) throws IOException {
    URL[] urls = new URL[files.length];
    
    for (int i = 0; i < urls.length; i++) {
      urls[i] = files[i].toURI().toURL();
    }
    
    return urls;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Copies a file to a directory preserving the file date.
   * <p>
   * This method copies the contents of the specified source file to a file of the same name in the specified destination directory. The destination directory is created if it does not exist. If the destination file exists, then this method will overwrite it.
   * 
   * @param srcFile
   *          an existing file to copy, must not be <code>null</code>
   * @param destDir
   *          the directory to place the copy in, must not be <code>null</code>
   * 
   * @throws NullPointerException
   *           if source or destination is null
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @see #copyFile(File, File, boolean)
   */
  public static void copyFileToDirectory(File srcFile, File destDir) throws IOException {
    copyFileToDirectory(srcFile, destDir, true);
  }
  
  /**
   * Copies a file to a directory optionally preserving the file date.
   * <p>
   * This method copies the contents of the specified source file to a file of the same name in the specified destination directory. The destination directory is created if it does not exist. If the destination file exists, then this method will overwrite it.
   * 
   * @param srcFile
   *          an existing file to copy, must not be <code>null</code>
   * @param destDir
   *          the directory to place the copy in, must not be <code>null</code>
   * @param preserveFileDate
   *          true if the file date of the copy should be the same as the original
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @see #copyFile(File, File, boolean)
   * @since Commons IO 1.3
   */
  public static void copyFileToDirectory(File srcFile, File destDir, boolean preserveFileDate) throws IOException {
    if (destDir == null) { throw new NullPointerException("Destination must not be null"); }
    if (destDir.exists() && destDir.isDirectory() == false) { throw new IllegalArgumentException("Destination '"
        + destDir + "' is not a directory"); }
    copyFile(srcFile, new File(destDir, srcFile.getName()), preserveFileDate);
  }
  
  /**
   * Copies a file to a new location preserving the file date.
   * <p>
   * This method copies the contents of the specified source file to the specified destination file. The directory holding the destination file is created if it does not exist. If the destination file exists, then this method will overwrite it.
   * 
   * @param srcFile
   *          an existing file to copy, must not be <code>null</code>
   * @param destFile
   *          the new file, must not be <code>null</code>
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @see #copyFileToDirectory(File, File)
   */
  public static void copyFile(File srcFile, File destFile) throws IOException {
    copyFile(srcFile, destFile, true);
  }
  
  /**
   * Copies a file to a new location.
   * <p>
   * This method copies the contents of the specified source file to the specified destination file. The directory holding the destination file is created if it does not exist. If the destination file exists, then this method will overwrite it.
   * 
   * @param srcFile
   *          an existing file to copy, must not be <code>null</code>
   * @param destFile
   *          the new file, must not be <code>null</code>
   * @param preserveFileDate
   *          true if the file date of the copy should be the same as the original
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @see #copyFileToDirectory(File, File, boolean)
   */
  public static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
    if (srcFile == null) { throw new NullPointerException("Source must not be null"); }
    if (destFile == null) { throw new NullPointerException("Destination must not be null"); }
    if (srcFile.exists() == false) { throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); }
    if (srcFile.isDirectory()) { throw new IOException("Source '" + srcFile + "' exists but is a directory"); }
    if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { throw new IOException("Source '" + srcFile
        + "' and destination '" + destFile + "' are the same"); }
    if (destFile.getParentFile() != null && destFile.getParentFile().exists() == false) {
      if (destFile.getParentFile().mkdirs() == false) { throw new IOException("Destination '" + destFile
          + "' directory cannot be created"); }
    }
    if (destFile.exists() && destFile.canWrite() == false) { throw new IOException("Destination '" + destFile
        + "' exists but is read-only"); }
    doCopyFile(srcFile, destFile, preserveFileDate);
  }
  
  /**
   * Internal copy file method.
   * 
   * @param srcFile
   *          the validated source file, must not be <code>null</code>
   * @param destFile
   *          the validated destination file, must not be <code>null</code>
   * @param preserveFileDate
   *          whether to preserve the file date
   * @throws IOException
   *           if an error occurs
   */
  private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
    if (destFile.exists() && destFile.isDirectory()) { throw new IOException("Destination '" + destFile
        + "' exists but is a directory"); }
    
    FileInputStream input = new FileInputStream(srcFile);
    try {
      FileOutputStream output = new FileOutputStream(destFile);
      try {
        IOUtils.copy(input, output);
      }
      finally {
        IOUtils.closeQuietly(output);
      }
    }
    finally {
      IOUtils.closeQuietly(input);
    }
    
    if (srcFile.length() != destFile.length()) { throw new IOException("Failed to copy full contents from '" + srcFile
        + "' to '" + destFile + "'"); }
    if (preserveFileDate) {
      destFile.setLastModified(srcFile.lastModified());
    }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Copies a directory to within another directory preserving the file dates.
   * <p>
   * This method copies the source directory and all its contents to a directory of the same name in the specified destination directory.
   * <p>
   * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source taking precedence.
   * 
   * @param srcDir
   *          an existing directory to copy, must not be <code>null</code>
   * @param destDir
   *          the directory to place the copy in, must not be <code>null</code>
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @since Commons IO 1.2
   */
  public static void copyDirectoryToDirectory(File srcDir, File destDir) throws IOException {
    if (srcDir == null) { throw new NullPointerException("Source must not be null"); }
    if (srcDir.exists() && srcDir.isDirectory() == false) { throw new IllegalArgumentException("Source '" + destDir
        + "' is not a directory"); }
    if (destDir == null) { throw new NullPointerException("Destination must not be null"); }
    if (destDir.exists() && destDir.isDirectory() == false) { throw new IllegalArgumentException("Destination '"
        + destDir + "' is not a directory"); }
    copyDirectory(srcDir, new File(destDir, srcDir.getName()), true);
  }
  
  /**
   * Copies a whole directory to a new location preserving the file dates.
   * <p>
   * This method copies the specified directory and all its child directories and files to the specified destination. The destination is the new location and name of the directory.
   * <p>
   * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source taking precedence.
   * 
   * @param srcDir
   *          an existing directory to copy, must not be <code>null</code>
   * @param destDir
   *          the new directory, must not be <code>null</code>
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @since Commons IO 1.1
   */
  public static void copyDirectory(File srcDir, File destDir) throws IOException {
    copyDirectory(srcDir, destDir, true);
  }
  
  /**
   * Copies a whole directory to a new location.
   * <p>
   * This method copies the contents of the specified source directory to within the specified destination directory.
   * <p>
   * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source taking precedence.
   * 
   * @param srcDir
   *          an existing directory to copy, must not be <code>null</code>
   * @param destDir
   *          the new directory, must not be <code>null</code>
   * @param preserveFileDate
   *          true if the file date of the copy should be the same as the original
   * 
   * @throws NullPointerException
   *           if source or destination is <code>null</code>
   * @throws IOException
   *           if source or destination is invalid
   * @throws IOException
   *           if an IO error occurs during copying
   * @since Commons IO 1.1
   */
  public static void copyDirectory(File srcDir, File destDir, boolean preserveFileDate) throws IOException {
    if (srcDir == null) { throw new NullPointerException("Source must not be null"); }
    if (destDir == null) { throw new NullPointerException("Destination must not be null"); }
    if (srcDir.exists() == false) { throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); }
    if (srcDir.isDirectory() == false) { throw new IOException("Source '" + srcDir + "' exists but is not a directory"); }
    if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath())) { throw new IOException("Source '" + srcDir
        + "' and destination '" + destDir + "' are the same"); }
    doCopyDirectory(srcDir, destDir, preserveFileDate);
  }
  
  /**
   * Internal copy directory method.
   * 
   * @param srcDir
   *          the validated source directory, must not be <code>null</code>
   * @param destDir
   *          the validated destination directory, must not be <code>null</code>
   * @param preserveFileDate
   *          whether to preserve the file date
   * @throws IOException
   *           if an error occurs
   * @since Commons IO 1.1
   */
  private static void doCopyDirectory(File srcDir, File destDir, boolean preserveFileDate) throws IOException {
    if (destDir.exists()) {
      if (destDir.isDirectory() == false) { throw new IOException("Destination '" + destDir
          + "' exists but is not a directory"); }
    }
    else {
      if (destDir.mkdirs() == false) { throw new IOException("Destination '" + destDir
          + "' directory cannot be created"); }
      if (preserveFileDate) {
        destDir.setLastModified(srcDir.lastModified());
      }
    }
    if (destDir.canWrite() == false) { throw new IOException("Destination '" + destDir + "' cannot be written to"); }
    // recurse
    File[] files = srcDir.listFiles();
    if (files == null) { // null if security restricted
      throw new IOException("Failed to list contents of " + srcDir);
    }
    for (int i = 0; i < files.length; i++) {
      File copiedFile = new File(destDir, files[i].getName());
      if (files[i].isDirectory()) {
        doCopyDirectory(files[i], copiedFile, preserveFileDate);
      }
      else {
        doCopyFile(files[i], copiedFile, preserveFileDate);
      }
    }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Copies bytes from the URL <code>source</code> to a file <code>destination</code>. The directories up to <code>destination</code> will be created if they don't already exist. <code>destination</code> will be overwritten if it already exists.
   * 
   * @param source
   *          the <code>URL</code> to copy bytes from, must not be <code>null</code>
   * @param destination
   *          the non-directory <code>File</code> to write bytes to (possibly overwriting), must not be <code>null</code>
   * @throws IOException
   *           if <code>source</code> URL cannot be opened
   * @throws IOException
   *           if <code>destination</code> is a directory
   * @throws IOException
   *           if <code>destination</code> cannot be written
   * @throws IOException
   *           if <code>destination</code> needs creating but can't be
   * @throws IOException
   *           if an IO error occurs during copying
   */
  public static void copyURLToFile(URL source, File destination) throws IOException {
    InputStream input = source.openStream();
    try {
      FileOutputStream output = openOutputStream(destination);
      try {
        IOUtils.copy(input, output);
      }
      finally {
        IOUtils.closeQuietly(output);
      }
    }
    finally {
      IOUtils.closeQuietly(input);
    }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Recursively delete a directory.
   * 
   * @param directory
   *          directory to delete
   * @throws IOException
   *           in case deletion is unsuccessful
   */
  public static void deleteDirectory(File directory) throws IOException {
    if (!directory.exists()) { return; }
    
    cleanDirectory(directory);
    if (!directory.delete()) {
      String message = "Unable to delete directory " + directory + ".";
      throw new IOException(message);
    }
  }
  
  /**
   * Clean a directory without deleting it.
   * 
   * @param directory
   *          directory to clean
   * @throws IOException
   *           in case cleaning is unsuccessful
   */
  public static void cleanDirectory(File directory) throws IOException {
    if (!directory.exists()) {
      String message = directory + " does not exist";
      throw new IllegalArgumentException(message);
    }
    
    if (!directory.isDirectory()) {
      String message = directory + " is not a directory";
      throw new IllegalArgumentException(message);
    }
    
    File[] files = directory.listFiles();
    if (files == null) { // null if security restricted
      throw new IOException("Failed to list contents of " + directory);
    }
    
    IOException exception = null;
    for (int i = 0; i < files.length; i++) {
      File file = files[i];
      try {
        forceDelete(file);
      }
      catch (IOException ioe) {
        exception = ioe;
      }
    }
    
    if (null != exception) { throw exception; }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Waits for NFS to propagate a file creation, imposing a timeout.
   * <p>
   * This method repeatedly tests {@link File#exists()} until it returns true up to the maximum time specified in seconds.
   * 
   * @param file
   *          the file to check, must not be <code>null</code>
   * @param seconds
   *          the maximum time in seconds to wait
   * @return true if file exists
   * @throws NullPointerException
   *           if the file is <code>null</code>
   */
  public static boolean waitFor(File file, int seconds) {
    int timeout = 0;
    int tick = 0;
    while (!file.exists()) {
      if (tick++ >= 10) {
        tick = 0;
        if (timeout++ > seconds) { return false; }
      }
      try {
        Thread.sleep(100);
      }
      catch (InterruptedException ignore) {
        // ignore exception
      }
      catch (Exception ex) {
        break;
      }
    }
    return true;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Reads the contents of a file into a String. The file is always closed.
   * 
   * @param file
   *          the file to read, must not be <code>null</code>
   * @param encoding
   *          the encoding to use, <code>null</code> means platform default
   * @return the file contents, never <code>null</code>
   * @throws IOException
   *           in case of an I/O error
   * @throws java.io.UnsupportedEncodingException
   *           if the encoding is not supported by the VM
   */
  public static String readFileToString(File file, String encoding) throws IOException {
    InputStream in = null;
    try {
      in = openInputStream(file);
      return IOUtils.toString(in, encoding);
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }
  
  /**
   * Reads the contents of a file into a String using the default encoding for the VM. The file is always closed.
   * 
   * @param file
   *          the file to read, must not be <code>null</code>
   * @return the file contents, never <code>null</code>
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.3.1
   */
  public static String readFileToString(File file) throws IOException {
    return readFileToString(file, null);
  }
  
  /**
   * Reads the contents of a file into a byte array. The file is always closed.
   * 
   * @param file
   *          the file to read, must not be <code>null</code>
   * @return the file contents, never <code>null</code>
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.1
   */
  public static byte[] readFileToByteArray(File file) throws IOException {
    InputStream in = null;
    try {
      in = openInputStream(file);
      return IOUtils.toByteArray(in);
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }
  
  /**
   * Reads the contents of a file line by line to a List of Strings. The file is always closed.
   * 
   * @param file
   *          the file to read, must not be <code>null</code>
   * @param encoding
   *          the encoding to use, <code>null</code> means platform default
   * @return the list of Strings representing each line in the file, never <code>null</code>
   * @throws IOException
   *           in case of an I/O error
   * @throws java.io.UnsupportedEncodingException
   *           if the encoding is not supported by the VM
   * @since Commons IO 1.1
   */
  public static List<String> readLines(File file, String encoding) throws IOException {
    InputStream in = null;
    try {
      in = openInputStream(file);
      return IOUtils.readLines(in, encoding);
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }
  
  /**
   * Reads the contents of a file line by line to a List of Strings using the default encoding for the VM. The file is always closed.
   * 
   * @param file
   *          the file to read, must not be <code>null</code>
   * @return the list of Strings representing each line in the file, never <code>null</code>
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.3
   */
  public static List<String> readLines(File file) throws IOException {
    return readLines(file, null);
  }
  
  // -----------------------------------------------------------------------
  /**
   * Writes a String to a file creating the file if it does not exist.
   * 
   * NOTE: As from v1.3, the parent directories of the file will be created if they do not exist.
   * 
   * @param file
   *          the file to write
   * @param data
   *          the content to write to the file
   * @param encoding
   *          the encoding to use, <code>null</code> means platform default
   * @throws IOException
   *           in case of an I/O error
   * @throws java.io.UnsupportedEncodingException
   *           if the encoding is not supported by the VM
   */
  public static void writeStringToFile(File file, String data, String encoding) throws IOException {
    OutputStream out = null;
    try {
      out = openOutputStream(file);
      IOUtils.write(data, out, encoding);
    }
    finally {
      IOUtils.closeQuietly(out);
    }
  }
  
  /**
   * Writes a String to a file creating the file if it does not exist using the default encoding for the VM.
   * 
   * @param file
   *          the file to write
   * @param data
   *          the content to write to the file
   * @throws IOException
   *           in case of an I/O error
   */
  public static void writeStringToFile(File file, String data) throws IOException {
    writeStringToFile(file, data, null);
  }
  
  /**
   * Writes a byte array to a file creating the file if it does not exist.
   * <p>
   * NOTE: As from v1.3, the parent directories of the file will be created if they do not exist.
   * 
   * @param file
   *          the file to write to
   * @param data
   *          the content to write to the file
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.1
   */
  public static void writeByteArrayToFile(File file, byte[] data) throws IOException {
    OutputStream out = null;
    try {
      out = openOutputStream(file);
      out.write(data);
    }
    finally {
      IOUtils.closeQuietly(out);
    }
  }
  
  /**
   * Writes the <code>toString()</code> value of each item in a collection to the specified <code>File</code> line by line. The specified character encoding and the default line ending will be used.
   * <p>
   * NOTE: As from v1.3, the parent directories of the file will be created if they do not exist.
   * 
   * @param file
   *          the file to write to
   * @param encoding
   *          the encoding to use, <code>null</code> means platform default
   * @param lines
   *          the lines to write, <code>null</code> entries produce blank lines
   * @throws IOException
   *           in case of an I/O error
   * @throws java.io.UnsupportedEncodingException
   *           if the encoding is not supported by the VM
   * @since Commons IO 1.1
   */
  public static void writeLines(File file, String encoding, Collection<?> lines) throws IOException {
    writeLines(file, encoding, lines, null);
  }
  
  /**
   * Writes the <code>toString()</code> value of each item in a collection to the specified <code>File</code> line by line. The default VM encoding and the default line ending will be used.
   * 
   * @param file
   *          the file to write to
   * @param lines
   *          the lines to write, <code>null</code> entries produce blank lines
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.3
   */
  public static void writeLines(File file, Collection<?> lines) throws IOException {
    writeLines(file, null, lines, null);
  }
  
  /**
   * Writes the <code>toString()</code> value of each item in a collection to the specified <code>File</code> line by line. The specified character encoding and the line ending will be used.
   * <p>
   * NOTE: As from v1.3, the parent directories of the file will be created if they do not exist.
   * 
   * @param file
   *          the file to write to
   * @param encoding
   *          the encoding to use, <code>null</code> means platform default
   * @param lines
   *          the lines to write, <code>null</code> entries produce blank lines
   * @param lineEnding
   *          the line separator to use, <code>null</code> is system default
   * @throws IOException
   *           in case of an I/O error
   * @throws java.io.UnsupportedEncodingException
   *           if the encoding is not supported by the VM
   * @since Commons IO 1.1
   */
  public static void writeLines(File file, String encoding, Collection<?> lines, String lineEnding) throws IOException {
    OutputStream out = null;
    try {
      out = openOutputStream(file);
      IOUtils.writeLines(lines, lineEnding, out, encoding);
    }
    finally {
      IOUtils.closeQuietly(out);
    }
  }
  
  /**
   * Writes the <code>toString()</code> value of each item in a collection to the specified <code>File</code> line by line. The default VM encoding and the specified line ending will be used.
   * 
   * @param file
   *          the file to write to
   * @param lines
   *          the lines to write, <code>null</code> entries produce blank lines
   * @param lineEnding
   *          the line separator to use, <code>null</code> is system default
   * @throws IOException
   *           in case of an I/O error
   * @since Commons IO 1.3
   */
  public static void writeLines(File file, Collection<?> lines, String lineEnding) throws IOException {
    writeLines(file, null, lines, lineEnding);
  }
  
  // -----------------------------------------------------------------------
  /**
   * Delete a file. If file is a directory, delete it and all sub-directories.
   * <p>
   * The difference between File.delete() and this method are:
   * <ul>
   * <li>A directory to be deleted does not have to be empty.</li>
   * <li>You get exceptions when a file or directory cannot be deleted. (java.io.File methods returns a boolean)</li>
   * </ul>
   * 
   * @param file
   *          file or directory to delete, must not be <code>null</code>
   * @throws NullPointerException
   *           if the directory is <code>null</code>
   * @throws IOException
   *           in case deletion is unsuccessful
   */
  public static void forceDelete(File file) throws IOException {
    if (file.isDirectory()) {
      deleteDirectory(file);
    }
    else {
      if (!file.exists()) { throw new FileNotFoundException("File does not exist: " + file); }
      if (!file.delete()) {
        String message = "Unable to delete file: " + file;
        throw new IOException(message);
      }
    }
  }
  
  /**
   * Schedule a file to be deleted when JVM exits. If file is directory delete it and all sub-directories.
   * 
   * @param file
   *          file or directory to delete, must not be <code>null</code>
   * @throws NullPointerException
   *           if the file is <code>null</code>
   * @throws IOException
   *           in case deletion is unsuccessful
   */
  public static void forceDeleteOnExit(File file) throws IOException {
    if (file.isDirectory()) {
      deleteDirectoryOnExit(file);
    }
    else {
      file.deleteOnExit();
    }
  }
  
  /**
   * Recursively schedule directory for deletion on JVM exit.
   * 
   * @param directory
   *          directory to delete, must not be <code>null</code>
   * @throws NullPointerException
   *           if the directory is <code>null</code>
   * @throws IOException
   *           in case deletion is unsuccessful
   */
  private static void deleteDirectoryOnExit(File directory) throws IOException {
    if (!directory.exists()) { return; }
    
    cleanDirectoryOnExit(directory);
    directory.deleteOnExit();
  }
  
  /**
   * Clean a directory without deleting it.
   * 
   * @param directory
   *          directory to clean, must not be <code>null</code>
   * @throws NullPointerException
   *           if the directory is <code>null</code>
   * @throws IOException
   *           in case cleaning is unsuccessful
   */
  private static void cleanDirectoryOnExit(File directory) throws IOException {
    if (!directory.exists()) {
      String message = directory + " does not exist";
      throw new IllegalArgumentException(message);
    }
    
    if (!directory.isDirectory()) {
      String message = directory + " is not a directory";
      throw new IllegalArgumentException(message);
    }
    
    File[] files = directory.listFiles();
    if (files == null) { // null if security restricted
      throw new IOException("Failed to list contents of " + directory);
    }
    
    IOException exception = null;
    for (int i = 0; i < files.length; i++) {
      File file = files[i];
      try {
        forceDeleteOnExit(file);
      }
      catch (IOException ioe) {
        exception = ioe;
      }
    }
    
    if (null != exception) { throw exception; }
  }
  
  /**
   * Make a directory, including any necessary but nonexistent parent directories. If there already exists a file with specified name or the directory cannot be created then an exception is thrown.
   * 
   * @param directory
   *          directory to create, must not be <code>null</code>
   * @throws NullPointerException
   *           if the directory is <code>null</code>
   * @throws IOException
   *           if the directory cannot be created
   */
  public static void forceMkdir(File directory) throws IOException {
    if (directory.exists()) {
      if (directory.isFile()) {
        String message = "File " + directory + " exists and is " + "not a directory. Unable to create directory.";
        throw new IOException(message);
      }
    }
    else {
      if (!directory.mkdirs()) {
        String message = "Unable to create directory " + directory;
        throw new IOException(message);
      }
    }
  }
  
  // -----------------------------------------------------------------------
  /**
   * Recursively count size of a directory (sum of the length of all files).
   * 
   * @param directory
   *          directory to inspect, must not be <code>null</code>
   * @return size of directory in bytes, 0 if directory is security restricted
   * @throws NullPointerException
   *           if the directory is <code>null</code>
   */
  public static long sizeOfDirectory(File directory) {
    if (!directory.exists()) {
      String message = directory + " does not exist";
      throw new IllegalArgumentException(message);
    }
    
    if (!directory.isDirectory()) {
      String message = directory + " is not a directory";
      throw new IllegalArgumentException(message);
    }
    
    long size = 0;
    
    File[] files = directory.listFiles();
    if (files == null) { // null if security restricted
      return 0L;
    }
    for (int i = 0; i < files.length; i++) {
      File file = files[i];
      
      if (file.isDirectory()) {
        size += sizeOfDirectory(file);
      }
      else {
        size += file.length();
      }
    }
    
    return size;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Tests if the specified <code>File</code> is newer than the reference <code>File</code>.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param reference
   *          the <code>File</code> of which the modification date is used, must not be <code>null</code>
   * @return true if the <code>File</code> exists and has been modified more recently than the reference <code>File</code>
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   * @throws IllegalArgumentException
   *           if the reference file is <code>null</code> or doesn't exist
   */
  public static boolean isFileNewer(File file, File reference) {
    if (reference == null) { throw new IllegalArgumentException("No specified reference file"); }
    if (!reference.exists()) { throw new IllegalArgumentException("The reference file '" + file + "' doesn't exist"); }
    return isFileNewer(file, reference.lastModified());
  }
  
  /**
   * Tests if the specified <code>File</code> is newer than the specified <code>Date</code>.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param date
   *          the date reference, must not be <code>null</code>
   * @return true if the <code>File</code> exists and has been modified after the given <code>Date</code>.
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   * @throws IllegalArgumentException
   *           if the date is <code>null</code>
   */
  public static boolean isFileNewer(File file, Date date) {
    if (date == null) { throw new IllegalArgumentException("No specified date"); }
    return isFileNewer(file, date.getTime());
  }
  
  /**
   * Tests if the specified <code>File</code> is newer than the specified time reference.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param timeMillis
   *          the time reference measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970)
   * @return true if the <code>File</code> exists and has been modified after the given time reference.
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   */
  public static boolean isFileNewer(File file, long timeMillis) {
    if (file == null) { throw new IllegalArgumentException("No specified file"); }
    if (!file.exists()) { return false; }
    return file.lastModified() > timeMillis;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Tests if the specified <code>File</code> is older than the reference <code>File</code>.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param reference
   *          the <code>File</code> of which the modification date is used, must not be <code>null</code>
   * @return true if the <code>File</code> exists and has been modified before the reference <code>File</code>
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   * @throws IllegalArgumentException
   *           if the reference file is <code>null</code> or doesn't exist
   */
  public static boolean isFileOlder(File file, File reference) {
    if (reference == null) { throw new IllegalArgumentException("No specified reference file"); }
    if (!reference.exists()) { throw new IllegalArgumentException("The reference file '" + file + "' doesn't exist"); }
    return isFileOlder(file, reference.lastModified());
  }
  
  /**
   * Tests if the specified <code>File</code> is older than the specified <code>Date</code>.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param date
   *          the date reference, must not be <code>null</code>
   * @return true if the <code>File</code> exists and has been modified before the given <code>Date</code>.
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   * @throws IllegalArgumentException
   *           if the date is <code>null</code>
   */
  public static boolean isFileOlder(File file, Date date) {
    if (date == null) { throw new IllegalArgumentException("No specified date"); }
    return isFileOlder(file, date.getTime());
  }
  
  /**
   * Tests if the specified <code>File</code> is older than the specified time reference.
   * 
   * @param file
   *          the <code>File</code> of which the modification date must be compared, must not be <code>null</code>
   * @param timeMillis
   *          the time reference measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970)
   * @return true if the <code>File</code> exists and has been modified before the given time reference.
   * @throws IllegalArgumentException
   *           if the file is <code>null</code>
   */
  public static boolean isFileOlder(File file, long timeMillis) {
    if (file == null) { throw new IllegalArgumentException("No specified file"); }
    if (!file.exists()) { return false; }
    return file.lastModified() < timeMillis;
  }
  
  // -----------------------------------------------------------------------
  /**
   * Computes the checksum of a file using the CRC32 checksum routine. The value of the checksum is returned.
   * 
   * @param file
   *          the file to checksum, must not be <code>null</code>
   * @return the checksum value
   * @throws NullPointerException
   *           if the file or checksum is <code>null</code>
   * @throws IllegalArgumentException
   *           if the file is a directory
   * @throws IOException
   *           if an IO error occurs reading the file
   * @since Commons IO 1.3
   */
  public static long checksumCRC32(File file) throws IOException {
    CRC32 crc = new CRC32();
    checksum(file, crc);
    return crc.getValue();
  }
  
  /**
   * Computes the checksum of a file using the specified checksum object. Multiple files may be checked using one <code>Checksum</code> instance if desired simply by reusing the same checksum object. For example:
   * 
   * <pre>
   * 
   * 
   * 
   * 
   * long csum = FileUtils.checksum(file, new CRC32()).getValue();
   * </pre>
   * 
   * @param file
   *          the file to checksum, must not be <code>null</code>
   * @param checksum
   *          the checksum object to be used, must not be <code>null</code>
   * @return the checksum specified, updated with the content of the file
   * @throws NullPointerException
   *           if the file or checksum is <code>null</code>
   * @throws IllegalArgumentException
   *           if the file is a directory
   * @throws IOException
   *           if an IO error occurs reading the file
   * @since Commons IO 1.3
   */
  public static Checksum checksum(File file, Checksum checksum) throws IOException {
    if (file.isDirectory()) { throw new IllegalArgumentException("Checksums can't be computed on directories"); }
    InputStream in = null;
    try {
      in = new CheckedInputStream(new FileInputStream(file), checksum);
      IOUtils.copy(in, NULL_OUTPUT_STREAM);// 把流读(拷贝)一次，才可以得到checksum
    }
    finally {
      IOUtils.closeQuietly(in);
    }
    return checksum;
  }
  
  /**
   * OutputStream的“Null Object”设计模式实现。
   */
  private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
    
    @Override
    @SuppressWarnings("unused")
    public void write(int b) throws IOException {}
  };
  
  // -------------- 我自己添加的方法
  
  /**
   * 递归得到指定文件夹下的指定文件，保留文件夹结构。
   * 
   * @param dir
   *          要遍历的文件夹
   * @param map
   *          匹配文件夹结构的Map，key=文件夹名，value=该文件夹下的文件列表
   * @param fileSuffix
   *          文件扩展名(格式：.xyz)，如果为null，则代表得到所有的文件
   */
  public static void listFiles(File dir, Map<File, Collection<File>> map, String fileSuffix) {
    checkDir(dir);
    Assert.notNull(map, "文件夹映射Map不能为Null");
    
    Collection<File> fileSet = new ArrayList<File>();
    map.put(dir, fileSet);
    for (File file : dir.listFiles()) {
      if (file.isDirectory()) {//如果是目录
        listFiles(file, map, fileSuffix);//递归调用，查下一层
      }
      else if (file.isFile()) {
        if (fileSuffix != null && !file.getName().endsWith(fileSuffix)) {//如果扩展名不符合
          continue;//放弃
        }
        fileSet.add(file);//添加文件
      }
    }
  }
  
  /**
   * 递归得到指定文件夹下的文件，不保留文件夹结构，仅仅是得到指定文件夹下的文件。
   * 
   * @param dir
   *          要遍历的文件夹
   * @param files
   *          指定文件夹下的文件列表
   * @param fileSuffix
   *          文件扩展名，如果为null，则代表得到所有的文件
   */
  public static void listFiles(File dir, Collection<File> files, String fileSuffix) {
    checkDir(dir);
    Assert.notNull(files, "文件列表不能为Null");
    
    for (File file : dir.listFiles()) {
      if (file.isDirectory()) {//如果是目录
        listFiles(file, files, fileSuffix);//递归调用，查下一层
      }
      else if (file.isFile()) {//如果是文件
        if (fileSuffix != null && !file.getName().endsWith(fileSuffix)) {//如果扩展名不符合
          continue;//放弃
        }
        files.add(file);//添加文件
      }
    }
  }
  
  private static JarFile makeJar(String jarPath) {
    JarFile jar = null;
    try {
      jar = new JarFile(jarPath);
    }
    catch (IOException e) {
      throw new RuntimeException("无法创建jar文件" + jarPath, e);
    }
    return jar;
  }
  
  /**
   * 遍历指定jar文件内的指定文件。
   * 
   * @param jarPath
   *          要遍历的jar文件路径
   * @param fileSuffix
   *          文件扩展名(格式：.xyz)，如果为null，则代表得到所有的文件
   * @return 找到的文件结构映射，key=文件路径，value=该文件的内容
   * @throws RuntimeException
   *           如果在遍历指定文件时出错
   */
  public static Map<String, byte[]> listFiles(String jarPath, String fileSuffix) {
    JarFile jar = makeJar(jarPath);
    String fileName = null;
    Map<String, byte[]> map = new HashMap<String, byte[]>();
    for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements();) {
      JarEntry entry = entries.nextElement();
      fileName = entry.getName();
      if (fileSuffix != null && !fileName.endsWith(fileSuffix)) {
        continue;
      }
      try {
        byte[] content = IOUtils.toByteArray(jar.getInputStream(entry));
        map.put(entry.getName(), content);
      }
      catch (Exception e) {
        throw new RuntimeException("提取 [" + fileName + "] 时出错: " + e);
      }
    }
    return map;
  }
  
  /**
   * 从指定jar文件中装载类。
   * <p style="color:red">
   * 这个方法只是尽可能的装载Class，如果某个类无法装载，则可能失败。
   * 
   * @param jarPath
   *          指定的jar文件路径
   * @param basePackName
   *          要装载的类的包名，该方法将装载该包及其子包下面所有的类
   * @return 符合条件的Class列表
   */
  public static List<Class<?>> loadClassFromJar(String jarPath, String basePackName) {
    List<Class<?>> classes = new ArrayList<Class<?>>();
    Map<String, byte[]> map = listFiles(jarPath, ".class");//先找出class
    StringBuilder name = new StringBuilder();
    for (String className : map.keySet()) {
      name.delete(0, name.length());
      name.append(className.replace('/', '.')).delete(name.length() - 6, name.length());//解析出类名
      if (name.toString().startsWith(basePackName)) {
        try {
          classes.add(Class.forName(name.toString()));//装载
        }
        catch (Exception e) {
          throw new RuntimeException("载入类 [" + name + "] 时出错", e);
        }
      }
    }
    return classes;
  }
  
  /**
   * 递归得到指定文件夹下的子文件夹，不保留文件夹结构，仅仅是得到指定文件夹下的子文件夹。
   * 
   * @param dir
   *          要遍历的文件夹
   * @param subDirs
   *          指定文件夹下的子文件夹列表
   */
  public static void listDirs(File dir, Collection<File> subDirs) {
    checkDir(dir);
    Assert.notNull(subDirs, "子文件夹列表不能为Null");
    for (File file : dir.listFiles()) {
      if (file.isDirectory()) {//如果是目录
        subDirs.add(file);//添加目录
        listDirs(file, subDirs);//递归调用，查下一层
      }
    }
  }
  
  /**
   * 递归得到指定文件夹下的所有文件夹，保留子文件夹结构。
   * 
   * @param dir
   *          要遍历的文件夹
   * @param subDirs
   *          匹配文件夹结构的Map，key=文件夹，value=该文件夹下的子文件夹列表
   */
  public static void listDirs(File dir, Map<File, Collection<File>> subDirs) {
    checkDir(dir);
    Assert.notNull(subDirs, "子文件夹列表不能为Null");
    List<File> fileSet = new ArrayList<File>();
    subDirs.put(dir, fileSet);//保留文件夹结构
    for (File file : dir.listFiles()) {
      if (file.isDirectory()) {//如果是目录
        fileSet.add(file);//添加目录
        listDirs(file, subDirs);//递归调用，查下一层
      }
    }
  }
  
  private static void checkDir(File dir) {
    Assert.notNull(dir, "指定文件夹不能为Null");
    Assert.isTrue(dir.exists() && dir.isDirectory(), "指定文件夹不是一个真实目录");
  }
  
  /**
   * 把数据写入磁盘。写入过程中发生的任何异常都将包装为RuntimeException抛出。
   * 
   * @param data
   *          要写入磁盘的数据
   * @param path
   *          目标路径
   * @param name
   *          保存的文件名
   * @param suffix
   *          文件后缀，格式<i>.xyz</i>
   */
  public static void writeDataToDisk(final byte[] data, String path, String name, String suffix) {
    if (!path.endsWith(File.separator)) {
      path = path + File.separator;
    }
    final String fullPath = path + name + suffix;
    handleFileWrite(fullPath, new Closure<FileChannel>() {
      
      @Override
      public void doWith(FileChannel fc) {
        ByteBuffer bb = ByteBuffer.wrap(data);
        try {
          fc.write(bb);
        }
        catch (IOException e) {
          throw new RuntimeException("写入 [" + fullPath + "] 时出错: " + e);
        }
      }
    });
  }
  
  /**
   * 从磁盘指定位置读取文件。读取过程中发生的任何异常都将包装为RuntimeException抛出。
   * 
   * @param path
   *          目标文件路径
   * @param name
   *          目标文件名
   * @param suffix
   *          目标文件后缀名
   * @return 文件的二进制内容数组
   */
  public static byte[] readFileFromDisk(String path, String name, String suffix) {
    if (!path.endsWith(File.separator)) {
      path = path + File.separator;
    }
    final String fullPath = path + name + suffix;
    byte[] fileContent = null;
    fileContent = handleFileRead(fullPath, new Processor<FileChannel, byte[]>() {
      
      @Override
      public byte[] process(FileChannel fc) {
        try {
          // create a ByteBuffer that is large enough
          // and read the contents of the file into it
          ByteBuffer bb = ByteBuffer.allocate((int) fc.size() + 1);
          fc.read(bb);
          byte[] content = new byte[bb.flip().limit()];// flip()后，limit就是读入数据的大小，position为0
          bb.get(content);
          return content;
        }
        catch (IOException e) {
          throw new RuntimeException("读取 [" + fullPath + "] 时出错:" + e);
        }
      }
    });
    
    return fileContent;
  }
  
  /**
   * 关闭文件通道。
   */
  public static void closeFileChannel(FileChannel channel) {
    if (channel != null) {
      try {
        channel.close();
      }
      catch (IOException e) {}
    }
  }
  
  /**
   * 使用类似Ruby处理文件的方式来处理文件读操作。
   * <p>
   * 调用者只需要提供文件路径和handler即可，方法会自动处理资源释放工作。
   * 
   * @param <T>
   *          需要从文件中得到的数据的类型
   * @param path
   *          文件路径，支持 file: classpath: 前缀
   * @param handler
   *          处理文件的闭包，根据传入的文件通道，返回需要的数据
   * @return 需要的返回值
   */
  public static <T> T handleFileRead(String path, Processor<FileChannel, T> handler) {
    FileInputStream is = null;
    T reValue = null;
    try {
      File file = ResourceUtils.getFile(path);
      Assert.isTrue(file.exists(), "文件 [" + path + "] 不存在");
      is = new FileInputStream(file);
      FileChannel channel = null;
      try {
        channel = is.getChannel();
        reValue = handler.process(channel);
      }
      finally {
        closeFileChannel(channel);
      }
    }
    catch (Exception e) {
      throw new RuntimeException("读取文件 [" + path + "] 时出错，嵌套异常为 " + e);
    }
    finally {
      IOUtils.closeQuietly(is);
    }
    return reValue;
  }
  
  /**
   * 使用类似Ruby处理文件的方式来处理文件写操作。
   * <p>
   * 调用者只需要提供文件路径和handler即可，方法会自动处理资源释放工作。
   * 
   * @param <T>
   *          需要从文件中得到的数据的类型
   * @param path
   *          文件路径，支持 file: classpath: 前缀
   * @param handler
   *          处理文件的闭包，根据传入的文件通道，处理文件的写入
   * @throws RuntimeException
   *           如果在handler处理通道时出错
   */
  public static void handleFileWrite(String path, Closure<FileChannel> handler) {
    FileOutputStream os = null;
    File file = null;
    try {
      file = ResourceUtils.getFile(path);
      os = new FileOutputStream(file);
      FileChannel channel = null;
      try {
        channel = os.getChannel();
        handler.doWith(channel);
      }
      finally {
        closeFileChannel(channel);
      }
    }
    catch (Exception e) {
      if (file.exists()) {// 如果是在handler.doWith(channel)时出错，则文件已经被创建，只是没有数据
        file.delete();// 空文件毫无意义，删除
      }
      throw new RuntimeException("写文件 [" + path + "] 时出错，嵌套异常为 " + e);
    }
    finally {
      IOUtils.closeQuietly(os);
    }
  }

}
